package de.hypoport.einarbeitung.scrumspirit.persistence.genericdao.jpa2;

import java.util.List;

import javax.persistence.EntityNotFoundException;
import javax.persistence.LockModeType;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.hypoport.einarbeitung.scrumspirit.persistence.genericdao.AbstractDao;

/**
 * Base class for JPA2 DAOs. To use, extend it in DAO classes and implement
 * {@link #getEntityManager()}.
 *
 * {@link #getEntityManager()} is externalized to allow subclasses to obtain the
 * correct entity manager, particularly when using multiple persistence units,
 * and in the necessary manner, e.g. specifying the unitName:
 *
 * <pre>
 * &#064;PersistenceContext(unitName = &quot;persistenceUnitName&quot;)
 * private EntityManager entityManager;
 * </pre>
 *
 * @param <K>
 *            The primary key class of the entity, e.g. Long.
 * @param <E>
 *            The entity class the DAO persists, e.g. Person.
 *
 * @see net.sf.sidaof.dao.jpa.Jpa2DaoImpl
 */
public abstract class AbstractJpa2Dao<K, E> extends AbstractDao<K, E> implements
        Jpa2Dao<K, E> {
    private static final Logger LOG = LoggerFactory
            .getLogger(AbstractJpa2Dao.class);

	private boolean needTransaction;

	@Override
	public boolean isNeedTransaction() {
		return needTransaction;
	}

	@Override
	public void setNeedTransaction(boolean needTransaction) {
		this.needTransaction = needTransaction;
	}

	/** Make a new instance, automatically determining the entity class. */
    public AbstractJpa2Dao() {
        super();
		needTransaction = true;
    }

    /**
	 * Make a new instance, specifying the entity class.
	 *
	 * @param entityClass
	 *            The entity class to manage.
	 */
    public AbstractJpa2Dao(Class<E> entityClass) {
        super(entityClass);
		needTransaction = true;
	}

	public AbstractJpa2Dao(Class<E> entityClass, boolean needTransaction) {
		super(entityClass);
		this.needTransaction = needTransaction;
    }


    @Override
    public E findById(K id) {
        LOG.debug("findById: id={}", id);

        if (id == null) {
            throw new IllegalArgumentException("id is null");
        }

        return getEntityManager().find(entityClass, id);
    }

    @Override
    public E findById(K id, LockModeType lockMode, boolean exceptionIfNotFound) {
        final Object[] logArgs = {id, lockMode, exceptionIfNotFound};
        LOG.debug("findById: id={}, lock={}, exceptionIfNotFound={}", logArgs);

        if (id == null) {
            throw new IllegalArgumentException("id is null");
        }

        E entity = findById(id);
        if (entity == null) {
            final String msg =
                    "Entity " + entityClass.getName() + " with id " + id
                            + " not found";
            LOG.info("getById: {}", msg);
            if (exceptionIfNotFound) {
                throw new EntityNotFoundException(msg);
            }
        } else if (lockMode != null) {
            // don't try lock if entity is null
            getEntityManager().lock(entity, lockMode);
        }
        return entity;
    }

	@SuppressWarnings("unchecked")
	@Override
    public List<E> findAll() {

		if (this.needTransaction) {
			LOG.debug("List<E> findAll(): begin transaction");
			getEntityManager().getTransaction().begin();
		}

		TypedQuery<E> query =
 (TypedQuery<E>) getEntityManager().createQuery(
				"SELECT m FROM " + entityClass.getSimpleName() + " m");
        LOG.debug("findAll: query={}", query);
		try {
			if (query != null) {
				List<E> list = query.getResultList();
				if (this.needTransaction) {
					LOG.debug("List<E> findAll(): commit transaction");
					getEntityManager().getTransaction().commit();
				}

				return list;
			}
			throw new Exception("Cant create Query");
		} catch (Exception e) {
			e.printStackTrace();
			if (this.needTransaction) {
				LOG.debug("List<E> findAll():: rollback transaction");
				getEntityManager().getTransaction().rollback();
			}
			return null;
		}
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<E> findAll(int startPosition, int maxResult) {
        LOG.debug("findAll: startPosition={}, maxResult={}", startPosition,
                maxResult);
        Query query =
                getEntityManager().createQuery("from " + entityClass.getName())
                        .setFirstResult(startPosition).setMaxResults(maxResult);
        LOG.debug("findAll: query={}", query);
        List<E> list = query.getResultList();
        return list;
    }

    @Override
    public void store(E entity) {
        LOG.debug("persist: entity={}", entity);
		if (this.needTransaction) {
			LOG.debug("persist: begin transaction");
			getEntityManager().getTransaction().begin();
		}
        getEntityManager().persist(entity);
		if (this.needTransaction) {
			LOG.debug("persist: commit transaction");
			getEntityManager().getTransaction().commit();
		}
        LOG.debug("persist: persisted entity={}", entity);
    }

    @Override
    public E merge(E entity) {
        LOG.debug("merge: entity={}", entity);
		if (this.needTransaction) {
			LOG.debug("merge: begin transaction");
			getEntityManager().getTransaction().begin();
		}
        E newEntity = getEntityManager().merge(entity);
        LOG.debug("merge: merged entity={}", newEntity);
		if (this.needTransaction) {
			LOG.debug("merge: commit transaction");
			getEntityManager().getTransaction().commit();
		}
        return newEntity;
    }

    @Override
    public void remove(E entity) {
		if (this.needTransaction) {
			LOG.debug("remove: begin transaction");
			getEntityManager().getTransaction().begin();
		}
        LOG.debug("remove: entity={}", entity);
        getEntityManager().remove(entity);
		if (this.needTransaction) {
			LOG.debug("remove: commit transaction");
			getEntityManager().getTransaction().commit();
		}
    }

    @Override
    public void clear() {
        LOG.debug("clear: called");
        getEntityManager().clear();
    }

    @Override
    public void flush() {
		if (getEntityManager().getTransaction().isActive()) {
			LOG.debug("flush: called");
			getEntityManager().flush();
		}
    }

    @Override
    public boolean contains(E entity) {
        LOG.debug("contains: entity={}", entity);
        boolean contains = getEntityManager().contains(entity);
        return contains;
    }

    @Override
    public void refresh(E entity) {
        LOG.debug("refresh: old entity={}", entity);
        getEntityManager().refresh(entity);
        LOG.debug("refresh: new entity={}", entity);
    }

    @Override
    public Long countAll(String where, String pathExpression) {
        if (where == null) {
            where = "";
        } else {
            where = " where " + where;
        }
        if (pathExpression == null) {
            pathExpression = "*";
        }

        final StringBuilder sb = new StringBuilder(1000);
        sb.append("select count(").append(pathExpression).append(") from ");
        sb.append(getEntityClass().getName());
        sb.append(" e");
        sb.append(where);

        String queryString = sb.toString();
        LOG.debug("countAll: queryString={}", queryString);

        Query q = getEntityManager().createQuery(queryString);
        Long count = (Long) q.getSingleResult();
        return count;
    }

    @Override
    public E runQueryWithSingleResult(Query q) {
        LOG.debug("runQueryWithSingleResult: query={}", q);
        return runQueryWithSingleResult(q, null, false);
    }

    @Override
    @SuppressWarnings("unchecked")
    // FIXME add boolean exceptionIfMultipleFound, default to true
    public E runQueryWithSingleResult(Query q, LockModeType lockMode,
            boolean exceptionIfNotFound) {
        final Object[] logArgs = {q, lockMode, exceptionIfNotFound};
        LOG.debug("runQueryWithSingleResult:"
                + " query={}, lock={}, exceptionIfNotFound={}", logArgs);

        E entity = null;
        try {
            entity = (E) q.getSingleResult();
            if (lockMode != null) {
                getEntityManager().lock(entity, lockMode);
            }
        } catch (NoResultException e) {
            handleQueryException(QUERY_SINGLERESULT_NOTFOUND_LOGMSG, e, q,
                    exceptionIfNotFound);
        } catch (NonUniqueResultException e) {
            handleQueryException(QUERY_SINGLERESULT_MULTFOUND_LOGMSG, e, q,
                    exceptionIfNotFound);
        }
        return entity;
    }

    protected void handleQueryException(String msg, PersistenceException e,
            Query q, boolean throwException) {
        Object[] logArgs = {entityClass.getName(), q, e};
        if (throwException) {
            LOG.error(msg, logArgs);
            throw e;
        } else {
            LOG.debug(msg, logArgs);
        }
    }
}
